1 // Licensed under the Apache License, Version 2.0 (the "License");
2 // you may not use this file except in compliance with the License.
3 // You may obtain a copy of the License at
4 //
5 // http://www.apache.org/licenses/LICENSE-2.0
6 //
7 // Unless required by applicable law or agreed to in writing, software
8 // distributed under the License is distributed on an "AS IS" BASIS,
9 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 // See the License for the specific language governing permissions and
11 // limitations under the License.
12
13 package org.apache.tapestry5.ioc.internal.util;
14
15 import org.apache.tapestry5.ioc.Invokable;
16
17 import java.util.concurrent.TimeUnit;
18 import java.util.concurrent.locks.ReadWriteLock;
19 import java.util.concurrent.locks.ReentrantReadWriteLock;
20
21 /**
22 * A barrier used to execute code in a context where it is guarded by read/write locks. In addition, handles upgrading
23 * read locks to write locks (and vice versa). Execution of code within a lock is in terms of a {@link Runnable} object
24 * (that returns no value), or a {@link Invokable} object (which does return a value).
25 */
26 public class ConcurrentBarrier
27 {
28 private final ReadWriteLock lock = new ReentrantReadWriteLock();
29
30 /**
31 * This is, of course, a bit of a problem. We don't have an avenue for ensuring that this ThreadLocal is destroyed
32 * at the end of the request, and that means a thread can hold a reference to the class and the class loader which
33 * loaded it. This may cause redeployment problems (leaked classes and class loaders). Apparently JDK 1.6 provides
34 * the APIs to check to see if the current thread has a read lock. So, we tend to remove the TL, rather than set its
35 * value to false.
36 */
37 private static class ThreadBoolean extends ThreadLocal<Boolean>
38 {
39 @Override
40 protected Boolean initialValue()
41 {
42 return false;
43 }
44 }
45
46 private final ThreadBoolean threadHasReadLock = new ThreadBoolean();
47
48 /**
49 * Invokes the object after acquiring the read lock (if necessary). If invoked when the read lock has not yet been
50 * acquired, then the lock is acquired for the duration of the call. If the lock has already been acquired, then the
51 * status of the lock is not changed.
52 *
53 * TODO: Check to see if the write lock is acquired and <em>not</em> acquire the read lock in that situation.
54 * Currently this code is not re-entrant. If a write lock is already acquired and the thread attempts to get the
55 * read lock, then the thread will hang. For the moment, all the uses of ConcurrentBarrier are coded in such a way
56 * that reentrant locks are not a problem.
57 *
58 * @param <T>
59 * @param invokable
60 * @return the result of invoking the invokable
61 */
62 public <T> T withRead(Invokable<T> invokable)
63 {
64 boolean readLockedAtEntry;
65
66 synchronized (threadHasReadLock)
67 {
68 readLockedAtEntry = threadHasReadLock.get();
69 }
70
71 if (!readLockedAtEntry)
72 {
73 lock.readLock().lock();
74
75 synchronized (threadHasReadLock)
76 {
77 threadHasReadLock.set(true);
78 }
79 }
80
81 try
82 {
83 return invokable.invoke();
84 }
85 finally
86 {
87 if (!readLockedAtEntry)
88 {
89 lock.readLock().unlock();
90
91 synchronized (threadHasReadLock)
92 {
93 threadHasReadLock.remove();
94 }
95 }
96 }
97 }
98
99 /**
100 * As with {@link #withRead(Invokable)}, creating an {@link Invokable} wrapper around the runnable object.
101 */
102 public void withRead(final Runnable runnable)
103 {
104 Invokable<Void> invokable = new Invokable<Void>()
105 {
106 @Override
107 public Void invoke()
108 {
109 runnable.run();
110
111 return null;
112 }
113 };
114
115 withRead(invokable);
116 }
117
118 /**
119 * Acquires the exclusive write lock before invoking the Invokable. The code will be executed exclusively, no other
120 * reader or writer threads will exist (they will be blocked waiting for the lock). If the current thread has a read
121 * lock, it is released before attempting to acquire the write lock, and re-acquired after the write lock is
122 * released. Note that in that short window, between releasing the read lock and acquiring the write lock, it is
123 * entirely possible that some other thread will sneak in and do some work, so the {@link Invokable} object should
124 * be prepared for cases where the state has changed slightly, despite holding the read lock. This usually manifests
125 * as race conditions where either a) some parallel unrelated bit of work has occured or b) duplicate work has
126 * occured. The latter is only problematic if the operation is very expensive.
127 *
128 * @param <T>
129 * @param invokable
130 */
131 public <T> T withWrite(Invokable<T> invokable)
132 {
133 boolean readLockedAtEntry = releaseReadLock();
134
135 lock.writeLock().lock();
136
137 try
138 {
139 return invokable.invoke();
140 }
141 finally
142 {
143 lock.writeLock().unlock();
144 restoreReadLock(readLockedAtEntry);
145 }
146 }
147
148 private boolean releaseReadLock()
149 {
150 boolean readLockedAtEntry;
151
152 synchronized (threadHasReadLock)
153 {
154 readLockedAtEntry = threadHasReadLock.get();
155 }
156
157 if (readLockedAtEntry)
158 {
159 lock.readLock().unlock();
160
161 synchronized (threadHasReadLock)
162 {
163 threadHasReadLock.set(false);
164 }
165 }
166
167 return readLockedAtEntry;
168 }
169
170 private void restoreReadLock(boolean readLockedAtEntry)
171 {
172 if (readLockedAtEntry)
173 {
174 lock.readLock().lock();
175
176 synchronized (threadHasReadLock)
177 {
178 threadHasReadLock.set(true);
179 }
180 }
181 else
182 {
183 synchronized (threadHasReadLock)
184 {
185 threadHasReadLock.remove();
186 }
187 }
188 }
189
190 /**
191 * As with {@link #withWrite(Invokable)}, creating an {@link Invokable} wrapper around the runnable object.
192 */
193 public void withWrite(final Runnable runnable)
194 {
195 Invokable<Void> invokable = new Invokable<Void>()
196 {
197 @Override
198 public Void invoke()
199 {
200 runnable.run();
201
202 return null;
203 }
204 };
205
206 withWrite(invokable);
207 }
208
209 /**
210 * Try to aquire the exclusive write lock and invoke the Runnable. If the write lock is obtained within the specfied
211 * timeout, then this method behaves as {@link #withWrite(Runnable)} and will return true. If the write lock is not
212 * obtained within the timeout then the runnable is never invoked and the method will return false.
213 *
214 * @param runnable Runnable object to execute inside the write lock.
215 * @param timeout Time to wait for write lock.
216 * @param timeoutUnit Units of timeout.
217 * @return true if lock was obtained and the runnable executed, or false otherwise.
218 */
219 public boolean tryWithWrite(final Runnable runnable, long timeout, TimeUnit timeoutUnit)
220 {
221 boolean readLockedAtEntry = releaseReadLock();
222
223 boolean obtainedLock = false;
224
225 try
226 {
227 try
228 {
229 obtainedLock = lock.writeLock().tryLock(timeout, timeoutUnit);
230
231 if (obtainedLock) runnable.run();
232
233 }
234 catch (InterruptedException e)
235 {
236 obtainedLock = false;
237 }
238 finally
239 {
240 if (obtainedLock) lock.writeLock().unlock();
241 }
242 }
243 finally
244 {
245 restoreReadLock(readLockedAtEntry);
246 }
247
248 return obtainedLock;
249 }
250
251 }